Skip to content

wireless/lpwan: add SX1301 concentrator, board support and SX127X fixes - #19675

Open
JorgeGzm wants to merge 1 commit into
apache:masterfrom
JorgeGzm:add_lora_gateway
Open

wireless/lpwan: add SX1301 concentrator, board support and SX127X fixes#19675
JorgeGzm wants to merge 1 commit into
apache:masterfrom
JorgeGzm:add_lora_gateway

Conversation

@JorgeGzm

@JorgeGzm JorgeGzm commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

nucleo-f746zg-lora-sx1301 b-l072z-lrwan1

Adds a character driver for the Semtech SX1301, the baseband processor of a
LoRaWAN gateway, together with the two SX125x radio front ends that it drives
through its internal SPI bridge. The driver resets the chip, loads the three
MCU firmware images, runs the calibration pass and the AGC handshake, and then
exposes the receive FIFO and the transmitter through read(), write() and a
small set of ioctls. Eight channel plans are provided, from AU915 to IN866, and
the firmware images are const so that they stay in flash instead of taking
24 KiB of RAM.

What an application sees is device independent. A concentrator has no radio
frequency, spreading factor or coding rate of its own, since those belong to
each packet, so the commands of the common wireless API have nothing to act
upon on such a device and the interface it needs is instead a channel plan, a
start and stop, and the counter downlinks are scheduled against. That contract
is nuttx/wireless/lpwan/lora_gw.h, with the commands allocated in the common
WLIOC_GW_* space rather than in a per chip block, and sx1301.h keeps only
sx1301_register() and the board hooks. A second concentrator driver
implements the same interface and the same application drives it; the coding
rate is the existing enum wlioc_lora_cr_e, and the rest of the packet keeps
the layout and the values of the userspace HAL that gateway software is
written against elsewhere, so that such an application ports by replacing
lgw_receive() with read() and lgw_send() with write().

Two details of the Semtech reference HAL are easy to miss, and both were found
on hardware:

  • The bridge that reaches the radios needs time between the chip select pulse
    and the read back register. A Linux host gets that for free from the cost of
    a spidev transaction; here the register writes are fast enough that the
    radios always read back as zero, which looks exactly like a dead board.
  • The frame synchronisation registers come up configured for a private
    network. A gateway that leaves them alone never detects a single frame of a
    public LoRaWAN device.

Board support is added for the Nucleo F746ZG carrying a shield of the
LRWAN_GS_HF1 family on SPI4, with a lorawan_gw configuration that brings up
Ethernet, DHCP and DNS along with the concentrator.

The SX127X driver gains what an SX1276 needs to talk to such a gateway, and
three defects found while making that work are fixed:

  • Frequency. SX127X_FSTEP divided by a step truncated to 61 Hz, while a
    step is FXOSC/2**19, about 61.035 Hz. The 0.06 per cent error puts a
    915 MHz channel more than 500 kHz away from the requested frequency, outside
    its own bandwidth: frames were transmitted, but on the wrong channel and
    undecodable by anything tuned correctly. Two NuttX radios still talked to
    each other, since both were wrong by the same amount.
  • Band. The high or low frequency front end is now selected from the
    configured frequency instead of being left at the reset value.
  • Unused function. sx127x_rx_watchdog() is only wired into the FSK and
    OOK receive path, but was compiled whenever receive support was on. A LoRa
    only configuration therefore failed to build with the flags of the CI. This
    is not hypothetical: nrf52840-dk:sx127x is such a configuration and does
    not build on master today (see Testing).

On STM32L0 the SYSCFG clock was never enabled, because the symbol that guarded
it does not exist in Kconfig for this chip. The mapping of a pin to an EXTI
line lives in SYSCFG_EXTICR, so every write to it was dropped and the mapping
stayed at its reset value: a GPIO interrupt on any port other than port A
silently never fired. nucleo-l073rz has DIO0 of its radio on PA10 and works
by accident, which is probably why this went unnoticed; the B-L072Z-LRWAN1 has
it on PB4, so a transmission never completed and the driver waited forever.
The same pattern is present in the STM32C0 and STM32G0 clock setup and was left
alone, as no board was available to test them.

On the B-L072Z-LRWAN1 the TCXO that clocks the radio is now powered before the
driver is registered, the user button is corrected to PB2 because PC13 carries
DIO3 of the radio on this board, and two configurations are added for use as a
LoRa transmitter.

Impact

New driver, new board support, and one fix per existing area:

  • drivers/wireless/lpwan/sx1301: new, off by default (LPWAN_SX1301).
  • drivers/wireless/lpwan/sx127x: the frequency and band fixes change the
    frequency actually used on the air, in the direction of the requested one.
    Anyone who compensated for the old error in an application will notice. The
    three new Kconfig options keep the previous behaviour by default (sync word
    0x12, 7.8 kHz bandwidth, spreading factor 7).
  • arch/arm/src/stm32l0: one clock enable. Every STM32L0 board gains working
    GPIO interrupts outside port A; nothing that worked before stops working.
  • Boards: nucleo-f746zg gains SPI4 and SPI5 pin definitions and the
    concentrator glue; b-l072z-lrwan1 gains the TCXO line and the corrected
    button pin.
  • Documentation: a page of its own for the gateway interface and one for the
    SX127X driver, under components/drivers/character/wireless/lpwan, with the
    two board pages trimmed to their wiring and pointing at them. This is what
    @cederom asked for in review: what is common to every board using a radio is
    written once, in one place, and referenced from the boards.

Testing

Host: Ubuntu 24.04.4 LTS, x86_64, Linux 7.0.0-28-generic

  • arm-none-eabi-gcc 13.2.1 (Arm GNU Toolchain 13.2.rel1)
  • xtensa-esp32-elf-gcc from the NuttX SDK, for the ESP32 board

Every build below uses the warning flags of the CI:
make EXTRAFLAGS="-Wno-cpp -Werror".

Build

Configuration text data bss
nucleo-f746zg:lorawan_gw (new) 342246 1568 37296
nucleo-f746zg:lorawan_gw + WIRELESS_LORA_PKT_FWD 362588 1680 41520
nucleo-f746zg:nsh 140010 888 4952
b-l072z-lrwan1:lorawan_tx (new) 81500 1072 3416
b-l072z-lrwan1:lorawan_beacon (new) 88408 1072 3424
b-l072z-lrwan1:sx127x 82824 1072 3432
nucleo-l073rz:sx127x 80268 1072 3404
nucleo-f091rc:sx127x 80556 1072 3408
nrf52840-dk:sx127x 101048 1084 6044
heltec_wifi_lora32:sx1276 116324 21040 9096

The lorawan_gw configuration does not select WIRELESS_LORA_PKT_FWD,
because that symbol comes from the companion pull request of nuttx-apps and
would make the defconfig of this repository diverge from what refresh.sh
produces against the master of the other one. The second line above is the
same configuration with the forwarder enabled from menuconfig, which is what
the documentation tells the user to do; the line can go into the defconfig once
the application is merged.

All of them build with no warning and no error. The last five are the other
users of the SX127X driver in the tree, built to show that the driver changes
do not regress them.

nrf52840-dk:sx127x does not build on master, which is what the unused
function fix addresses:

$ make -j EXTRAFLAGS="-Wno-cpp -Werror"
wireless/lpwan/sx127x/sx127x.c:1291:13: error: 'sx127x_rx_watchdog' defined but
not used [-Werror=unused-function]

nxstyle reports nothing on the changed files, refresh.sh leaves every
touched configuration unchanged, and the documentation builds with
sphinx -W.

Concentrator, on hardware

Nucleo F746ZG with a RisingHF RHF0M301 shield, lorawan_gw:

nsh> lora start
sx1301_reg_probe: SX1301 detected, version 0x67
sx1301_setup_radio: Radio A: SX125x version 0x21
sx1301_setup_radio: Radio A: PLL locked at 917100000 Hz
sx1301_setup_radio: Radio B: PLL locked at 917900000 Hz
sx1301_calibrate: Calibration done, status 0xbf
sx1301_start: Arbiter firmware version 1 (expected 1)
sx1301_agc_start: AGC running, radio map 0xf0
sx1301_start: Concentrator started, modems 0x0b

Receiving a LoRaWAN device

An end device based on an SX1276 sending its join request, AU915 first
sub-band:

sx1301_receive: RX chain 0 SF10 915200000 Hz snr 14.0 dB size 23 status 0x10
sx1301_receive: RX chain 3 SF10 915800000 Hz snr 13.7 dB size 23 status 0x10

Status 0x10 is a valid CRC, and 23 bytes is the exact length of a join
request. Forwarded to a network server over the internet with the forwarder of
the companion pull request in nuttx-apps:

lora: forwarding to au1.cloud.thethings.network (up 1700, down 1700)
lora: forwarded 1 packet(s)
RX OK: 1   RX FWD: 1   PUSH: 4 sent, 3 acked   PULL: 19 sent, 18 acked

Receiving an SX127X, which is what the SX127X fixes are for

B-L072Z-LRWAN1 running lorawan_tx and transmitting on 917.2 MHz with
sx127x -m 0 -f 917200000 -t -p 0 -l 32.

Before the frequency fix the frames arrived on the channel 600 kHz above the
requested one and never passed the CRC:

sx1301_receive: RX chain 5 SF7 917800000 Hz snr 0.2 dB size 32 status 0x11
sx1301_receive: RX chain 5 SF7 917800000 Hz snr -1.7 dB size 32 status 0x11

After it, on the requested channel and with a valid CRC:

sx1301_receive: RX chain 2 SF7 917200000 Hz snr 8.2 dB size 32 status 0x10
sx1301_receive: RX chain 2 SF7 917200000 Hz snr 8.5 dB size 32 status 0x10

Before the SYSCFG fix the same board sent one frame per reset and then blocked
forever waiting for the transmit interrupt; after it, 509 frames in 30 s. The
mapping register, read from the running firmware, confirms why:

nsh> xd 0x4001000c 4        # SYSCFG_EXTICR2, holds EXTI line 4
0000: 00 00 00 00           # before: line 4 on port A, while DIO0 is PB4
0000: 01 00 00 00           # after:  line 4 on port B

Transmitting

The concentrator sends a packet with lora tx 917200000 7 hello-from-gateway
and two B-L072Z-LRWAN1 boards listening on that frequency receive it:

board A> Received:  SNR = 6  RSSI = -29  len = 18
board B> Received:  SNR = 7  RSSI = -30  len = 18
         0x68 0x65 0x6c 0x6c 0x6f 0x2d 0x66 0x72 0x6f 0x6d

Five transmissions, five receptions on each board.

The Docker based CI lists were not run locally; they are left to the CI of the
pull request.

@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Area: Drivers Drivers issues Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces. Board: arm labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

xiaoxiang781216
xiaoxiang781216 previously approved these changes Aug 4, 2026
acassis
acassis previously approved these changes Aug 4, 2026
Comment thread Documentation/platforms/arm/stm32l0/boards/b-l072z-lrwan1/index.rst
Comment thread Documentation/platforms/arm/stm32f7/boards/nucleo-f746zg/index.rst
cederom
cederom previously approved these changes Aug 4, 2026

@cederom cederom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @JorgeGzm :-)

I would just put dedicated documentation page for lora / sx / etc and then reference it from a board page. We has too many details scattered across different board, would be nice to have all in one place and then just reference :-)

@JorgeGzm
JorgeGzm dismissed stale reviews from cederom and acassis via e9141d6 August 4, 2026 16:21
Comment thread Documentation/platforms/arm/stm32f7/boards/nucleo-f746zg/index.rst
Comment thread Documentation/components/drivers/character/wireless/lpwan/lora_gw.rst Outdated
cederom
cederom previously approved these changes Aug 4, 2026

@cederom cederom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the documentation updates @JorgeGzm :-)

Add a character driver for the Semtech SX1301, the baseband processor of
a LoRaWAN gateway, together with the two SX125x radio front ends that it
drives through its internal SPI bridge.  The driver resets the chip,
loads the three MCU firmware images, runs the calibration pass and the
AGC handshake, and then exposes the receive FIFO and the transmitter
through read(), write() and a small set of ioctls.  Eight channel plans
are provided, from AU915 to IN866, and the firmware images are const so
that they stay in flash instead of taking 24 KiB of RAM.

What an application sees is device independent.  A concentrator has no
radio frequency, spreading factor or coding rate of its own, since those
belong to each packet, so the commands of the common wireless API have
nothing to act upon and the interface is instead a channel plan, a start
and stop, and the counter downlinks are scheduled against.  That contract
lives in nuttx/wireless/lpwan/lora_gw.h with the commands allocated in the
common WLIOC_GW space rather than in a per chip block, and sx1301.h keeps
only the registration function and the board hooks, so a second
concentrator driver implements the same interface and the same application
drives it.  The coding rate is the existing enum wlioc_lora_cr_e; the rest
of the packet keeps the layout and the values of the userspace HAL that
gateway software is written against elsewhere.

Two details of the reference HAL are easy to miss, and both were found on
hardware:

  - The bridge that reaches the radios needs time between the chip select
    pulse and the read back register.  A Linux host gets that for free
    from the cost of a spidev transaction; here the register writes are
    fast enough that the radios always read back as zero, which looks
    exactly like a dead board.
  - The frame synchronisation registers come up configured for a private
    network.  A gateway that leaves them alone never detects a single
    frame of a public LoRaWAN device.

Packets whose CRC failed, or that carried no CRC at all, are counted and
dropped rather than delivered, as the reference packet forwarder does,
and the timestamp of a received packet gets the correction of the
reference HAL so that a downlink lands inside the receive window of the
device.

Board support is added for the Nucleo F746ZG carrying a shield of the
LRWAN_GS_HF1 family on SPI4, with a lorawan_gw configuration that brings
up Ethernet, DHCP and DNS along with the concentrator.

The SX127X driver gains what an SX1276 needs to talk to such a gateway,
and two bugs found while making that work are fixed:

  - The frequency of the synthesiser was computed by dividing by a
    truncated step of 61 Hz, while a step is FXOSC/2**19, about 61.035 Hz.
    The 0.06 per cent error puts a 915 MHz channel more than 500 kHz away
    from where it was asked for, which is outside its bandwidth: the
    frames were transmitted, but on the wrong channel and undecodable.
    The conversion now scales first and divides afterwards.
  - On STM32L0 the SYSCFG clock was never enabled, because the symbol
    that guarded it does not exist in Kconfig for this chip.  The mapping
    of a pin to an EXTI line lives in SYSCFG_EXTICR, so every write to it
    was dropped and the mapping stayed at its reset value: a GPIO
    interrupt on any port other than port A silently never fired.  On this
    board that is DIO0 of the radio, so a transmission never completed and
    the driver waited forever.  The same pattern is present in the STM32C0
    and STM32G0 clock setup and was left alone, as no board was available
    to test them.

Beyond that, the high or low band of the front end is now selected from
the frequency rather than left at the reset value, and the LoRa sync word,
bandwidth and spreading factor became configurable, since the ioctls that
would set them at runtime are not implemented.  On the B-L072Z-LRWAN1 the
TCXO that clocks the radio is now powered before the driver is registered,
the user button is corrected to PB2 because PC13 carries DIO3 of the radio
on this board, and two configurations are added for use as a LoRa
transmitter.

The gateway interface and the SX127X driver each get a page of their own
under Documentation, so that what is common to every board using them is
written once and the board pages only describe their wiring and refer to
it.  Those references go through a label at the top of each page rather
than through its path, so that moving a document later does not break
them.

Tested with both boards: the concentrator starts, both radio PLLs lock,
and the join request of a LoRaWAN device is received with a valid CRC and
forwarded to a network server.  With the fixes above, an SX1276 on the
B-L072Z-LRWAN1 transmits continuously and its frames arrive on the
expected channel of the gateway with a valid CRC and a signal to noise
ratio of about 8 dB.  Two of those boards also talk to each other in both
directions with the sx127x example, which exercises the receive path and
its completion interrupt as well.

Signed-off-by: Jorge Guzman <jorge.gzm@gmail.com>
Assisted-by: Claude Code 4.8
}
};

#define SX1301_NREGIONS (sizeof(g_sx1301_regions) / \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use nitems directly

#define MULTI(f, rf) \
{ \
.freq_hz = (f), .rf_chain = (rf), \
.type = LORA_GW_CHAN_MULTI_SF, \

@xiaoxiang781216 xiaoxiang781216 Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

align \

include wireless/lpwan/sx127x/Make.defs
include wireless/lpwan/sx126x/Make.defs
include wireless/lpwan/rn2xx3/Make.defs
include wireless/lpwan/sx1301/Make.defs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move before line 29

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: Drivers Drivers issues Board: arm Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants